feat: nested CORS iframes, ignore controls, and closed shadow DOM#312
Open
aryanku-dev wants to merge 12 commits into
Open
feat: nested CORS iframes, ignore controls, and closed shadow DOM#312aryanku-dev wants to merge 12 commits into
aryanku-dev wants to merge 12 commits into
Conversation
Replace the flat top-level iframe loop with a recursive `processFrameTree` that switches into each cross-origin iframe, captures its DOM, and descends into any further cross-origin iframes nested inside it (up to a configurable depth). Cycles are detected by tracking the chain of ancestor frame URLs and skipping any frame whose `src` already appears in the chain — without this guard, pages that link to each other could produce up to `maxIframeDepth` duplicate corsIframes entries. The depth cap defaults to 5 (matching the canonical Percy SDK behaviour) and is configurable per-snapshot via `maxIframeDepth` or via `cliConfig.snapshot.maxIframeDepth`. Inputs are clamped to a 1..10 range through `clampFrameDepth`. Nested-frame origin is compared against the IMMEDIATE PARENT origin (not the top page origin) so a same-origin grandchild inside a cross-origin parent is correctly inlined by PercyDOM and a cross-origin grandchild inside a same-origin parent is still captured. Mirrors percy/percy-nightwatch#869 and percy/percy-playwright#609. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Skip iframes that carry the `data-percy-ignore` boolean attribute when enumerating both top-level and nested cross-origin iframes. Customers add this attribute to opt out of CORS iframe capture for a specific frame without having to maintain a selector list — useful for ad slots or analytics iframes whose contents are noisy. Selenium's `getAttribute` returns an empty string for boolean attributes with no value, so a non-null result is treated as a positive hit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Customers can now pass an `ignoreIframeSelectors` list (either in the per-snapshot options Map or via `cliConfig.snapshot.ignoreIframeSelectors`) to skip any cross-origin iframe whose element matches one of the supplied CSS selectors. Matching is performed in-browser via `Element.matches` so any selector the browser accepts is valid; invalid selectors are tolerated without aborting the snapshot. Inputs go through `normalizeIgnoreSelectors` which accepts a List<String>, a single String, or null and yields a sanitised List<String> with empty/ whitespace-only entries removed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After switching into a cross-origin iframe, read `document.URL` and run the unsupported-src check again. The parent-side `src` attribute can be stale or misleading — the frame may have failed to load (leaving an about:blank document), or been navigated by script after attach to a data:/javascript: URL. Skipping these post-switch avoids attempting to serialize a placeholder document. When a post-switch URL is available it is also reported as the captured `frameUrl` and used as the parent context for any nested CORS iframe enumeration. Falls back to the parent-side `src` when the executor returns a non-String value (e.g. under mocking). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ostException When the driver fails to step back to a parent frame after recursing into a nested cross-origin iframe, we previously lost everything captured so far (a flaky network call inside a depth-3 frame would forfeit even the depth-1 snapshot). Introduce `PercyContextLostException` which carries a `partialCapture` list of every iframe snapshot collected before the failure; each recursion layer appends its own captures to the carried list and re-throws, and the top-level loop in `getSerializedDOM` merges the recovered captures into the snapshot and falls back to default content before aborting further sibling enumeration. Mirrors the `percyContextLost` flag in percy/percy-nightwatch#869 and percy/percy-webdriverio#... so the wire-format output stays consistent across SDKs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closed shadow roots (`{mode: 'closed'}`) are invisible to JavaScript —
`element.shadowRoot` is `null` and there is no API that returns the
underlying ShadowRoot object. The PercyDOM serializer can pierce them
through a window-bound `__percyClosedShadowRoots` WeakMap (host element
→ shadow root) populated before serialization, but Selenium has no way
to obtain the closed shadow root from page script.
Use Chrome DevTools Protocol to discover and resolve them:
1. `DOM.getDocument {depth: -1, pierce: true}` to walk the entire DOM
tree including closed shadow subtrees.
2. For each closed shadow root, `DOM.resolveNode` on the host and the
shadow root to obtain JS object handles.
3. `Runtime.callFunctionOn` to write the pair into the WeakMap.
`contentDocument` nodes are skipped because their execution context is
distinct and has no WeakMap. Non-Chromium drivers are detected with a
single `instanceof ChromeDriver` check and silently fall through, so the
SDK keeps working with Firefox/WebKit without changes.
Mirrors percy/percy-playwright#609.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add JUnit + Mockito unit tests for the new helper methods and the nested cross-origin iframe capture flow: - `clampFrameDepth` bounds + defaults - `normalizeIgnoreSelectors` accepts List<String> / String / null - `resolveMaxFrameDepth` precedence (option > cliConfig > default) - `resolveIgnoreSelectors` precedence - `data-percy-ignore` iframes are skipped without `switchTo` - `ignoreIframeSelectors` matches are skipped without `switchTo` - `processFrame` bails after switch when document.URL is unsupported - `PercyContextLostException.partialCapture` round-trips - `getSerializedDOM` recovers partial captures on context loss - `exposeClosedShadowRoots` is a no-op for non-Chrome drivers - `collectClosedShadowPairs` walks the CDP tree and skips iframes Tests live in a separate `IframeFeatureTest` class to avoid being blocked by `SdkTest`'s `@BeforeAll` Firefox initialisation in environments without a Firefox binary. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…anup - responsive snapshot detection threw NPE when cliConfig.snapshot was missing or JSON-null; guard each layer before reading responsiveSnapshotCapture. - getSerializedDOM treated a null jse return as a Map and ClassCastException'd deep in the snapshot path. Detect non-Map results and raise a clear error pointing at the @percy/dom load failure as the root cause. - Pair every successful DOM.enable with DOM.disable in a finally block so the CDP session doesn't keep emitting DOM events after closed-shadow capture. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Null-safe origin equality in both top-level and nested iframe comparisons. Switched to Objects.equals so a child URI that resolves to no host (data:, mailto:, schemeless) can never trigger an NPE that escapes the per-iframe catch. - Document the clampFrameDepth semantic: maxIframeDepth=0 falls back to DEFAULT_MAX_FRAME_DEPTH (5), mirroring @percy/sdk-utils. Disabling CORS capture should use ignoreIframeSelectors or data-percy-ignore, not depth=0. Comment guards against a silent flip in future refactors. - Expose closed shadow roots inside each CORS frame after switchTo() — mirrors the top-page behaviour so closed shadow DOM inside cross- origin iframes is also captured. Per-pair try/catch in the existing helper keeps one bad backendNodeId from aborting the rest. TODO tracks moving to per-frame CDP sessions when BiDi stabilises. - Remove the dead processFrame method — fully replaced by processFrameTree. Keeping duplicate logic invited drift. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- nestedIframeWithNullOriginIsNullSafeAndDoesNotAbortLoop: regression test for the NPE risk at the child-origin comparison; a data:... child must not abort the outer CORS frame capture. - clampFrameDepthZeroReturnsDocumentedDefault: semantic guard so any future change to treat 0 as "disable" trips a test. - exposeClosedShadowRootsIsAttemptedInsideCorsFrame: confirms the per-CORS-frame helper invocation and the TODO marker survive future refactors; ensures the call is safe on a non-Chrome driver. - collectClosedShadowPairsContinuesPastOneBadEntry: documents that the collector tolerates missing backendNodeId fields without throwing, and one bad pair does not abort the rest at runtime. - Update the post-switch unsupported-URL test to drive processFrameTree directly (processFrame removed). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Picks up the latest CLI patch series for parity with sibling SDKs and to unblock the external percy/percy-java-selenium status check. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The legacy processFrame(WebElement, Map) helper was removed as dead code once processFrameTree subsumed it. The reflection-based unit test still called the old name and failed with NoSuchMethodException in CI. Rewrite it to drive the same skip-when-percyElementId-missing path through processFrameTree, asserting an empty result and that the driver is never switched into the frame. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Brings percy-selenium-java to parity with the canonical Percy CORS iframe + closed shadow DOM feature set.
Implemented
data-percy-ignoreattribute opt-outignoreIframeSelectorsoptionisUnsupportedIframeSrcPercyContextLostExceptionrecovery mergespartialCaptureexposeClosedShadowRoots)clampFrameDepth,normalizeIgnoreSelectors,resolveMaxFrameDepth,resolveIgnoreSelectors)Skipped
Reference
Mirrored from percy/percy-nightwatch#869 (PER-7292-add-cors-iframe-support); CDP from percy/percy-playwright#609.
Test plan
IframeFeatureTest(11 tests) and existingCacheTest(3 tests) pass undermvn test.SdkTest's integration tests require a local Firefox binary (@BeforeAllinstantiatesFirefoxDriver). They were not exercised in the sync environment because Firefox is not installed; this matches the pre-existing baseline onmasterand is not a regression introduced by this PR.🤖 Generated with Claude Code via /percy-sdk-sync